﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace ProjectN.Core.Instance
{
    using static JsonSerialization.SerializationHelper;

    public interface IDataModel
    {
        Guid Guid { get; init; }
        event Action? OnUpdate;
    }

    public class FileManager<T> : IDisposable where T : class, IDataModel, new()
    {
        private readonly string directory;
        private readonly IndexFile index;
        private const string IndexFileName = "index.json";
        private readonly Dictionary<Guid, Handler<T>> handlerCache = [];
        private bool disposedValue;

        public Guid? PrimaryGuid => index.Primary;

        internal IEnumerable<Guid> AllPages
        {
            get
            {
                foreach (var guid in index.Files)
                    yield return guid;
            }
        }

        internal IEnumerable<Guid> CachedPages
        {
            get
            {
                foreach (var guid in handlerCache.Keys)
                    yield return guid;
            }
        }
        internal bool TryGetCache(Guid guid, [MaybeNullWhen(false)] out Handler<T> handler)
            => handlerCache.TryGetValue(guid, out handler);

        public FileManager(string path)
        {
            directory = path;
            Directory.CreateDirectory(path);

            var indexPath = Path.Combine(path, IndexFileName);
            index = File.Exists(indexPath) ?
                JsonConvert.DeserializeObject<IndexFile>(File.ReadAllText(indexPath)) ?? new():
                new();
        }

        public bool Contains(Guid guid) => index.Files.Contains(guid);

        private Handler<T> RequireHandler(Guid guid)
        {
            if (!Contains(guid))
                throw new InvalidOperationException("Guid has NOT Existed yet.");
            if (handlerCache.TryGetValue(guid, out var handle))
                return handle;
            return handlerCache[guid] = new(this, guid);
        }

        public Handler<T> Create(Guid? guid = null)
        {
            var GUID = guid ?? Guid.NewGuid();

            index.Files.Add(GUID);
            SaveIndex();

            return RequireHandler(GUID);
        }

        public Handler<T> Get(Guid guid) => RequireHandler(guid);

        public void Remove(Guid guid)
        {
            if (!index.Files.Contains(guid))
                return;

            File.Delete(GetFilePath(guid));
            index.Files.Remove(guid);

            if (index.Primary == guid)
                index.Primary = Guid.Empty;

            SaveIndex();
        }

        internal void SetPrimary(Guid guid)
        {
            if (!index.Files.Contains(guid))
                throw new KeyNotFoundException("Guid not found");

            index.Primary = guid;
            SaveIndex();
        }

        internal string GetFilePath(Guid guid) => Path.Combine(directory, $"{guid}.json");

        private void SaveIndex()
        {
            File.WriteAllText(
                Path.Combine(directory, IndexFileName),
                JsonConvert.SerializeObject(index)
            );
        }

        public void SaveAll()
        {
            foreach (var handler in handlerCache.Values)
                handler.Save();
        }

        private class IndexFile()
        {
            public Collection<Guid> Files { get; set; } = [];
            public Guid? Primary { get; set; } = null;
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    SaveAll();
                    handlerCache.Clear();
                }

                disposedValue = true;
            }
        }

        ~FileManager()
        {
            Dispose(disposing: false);
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }

    public class Handler<T> where T : class, IDataModel, new()
    {
        private readonly FileManager<T> manager;
        private readonly Lazy<T> lazyData;

        public T Data => lazyData.Value;

        public Handler(FileManager<T> manager, Guid guid)
        {
            this.manager = manager;
            lazyData = new Lazy<T>(() => Load(guid));
        }

        private T Load(Guid guid)
        {
            T data;
            var path = manager.GetFilePath(guid);

            if (File.Exists(path))
            {
                using var stream = File.OpenRead(path);
                data = ReadFromStream<T>(stream)
                    ?? throw new InvalidOperationException("Failed to read data");
            }
            else
            {
                data = new T() { Guid = guid };
            }

            data.OnUpdate += Save;
            return data;
        }

        public void Save()
        {
            using var stream = File.OpenWrite(manager.GetFilePath(Data.Guid));
            WriteToStream(stream, Data);
        }

        public void SetAsPrimary()
        {
            manager.SetPrimary(Data.Guid);
        }

        public static implicit operator Guid(Handler<T> handler) => handler.Data.Guid;
        public static implicit operator T(Handler<T> handler) => handler.Data;
    }
}
